package com.mazaiting.socket.client;

import cn.hutool.core.io.IoUtil;
import com.mazaiting.common.core.domain.result.ResultCode;
import com.mazaiting.common.core.ex.exceptions.ConfigException;
import com.mazaiting.common.core.ex.exceptions.ValidationException;
import com.mazaiting.common.core.utils.StringUtil;
import com.mazaiting.socket.client.entity.ClientConfig;
import com.mazaiting.socket.enums.Operation;
import com.mazaiting.socket.util.NioUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;

/**
 * Nio 客户端
 *
 * @author mazaiting
 * @create_time 2022/4/18 11:48
 */
public class NioClient {

    /**
     * 日志工具
     */
    private final Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * 选择器
     */
    private Selector selector = null;

    /**
     * 客户端配置
     */
    private final ClientConfig clientConfig;

    /**
     * 待发送的数据
     */
    private String data;

    /**
     * 保证可见性不保证事务性。  就是为了在并发场景中，快速的共享数据。
     * true: 发送结束
     */
    private volatile boolean flag = false;

    public NioClient(ClientConfig clientConfig, String data) throws ConfigException, ValidationException {
        // 判断 IP 是否为空
        if (StringUtil.isEmpty(clientConfig.getIp())) {
            throw new ConfigException(ResultCode.SOCKET_CLIENT_IP_ERROR);
        }
        // 判断端口是否为空
        if (Objects.isNull(clientConfig.getPort())) {
            throw new ConfigException(ResultCode.SOCKET_CLIENT_PORT_ERROR);
        }
        this.clientConfig = clientConfig;
        setData(data);
    }

    /**
     * 启动
     */
    public void send() {
        try {
            // 打开选择器与通道
            selector = Selector.open();
            SocketChannel clientChannel = SocketChannel.open();
            NioUtil.registerChannel(selector, clientChannel, Operation.CONNECT);
            clientChannel.connect(new InetSocketAddress(clientConfig.getIp(), clientConfig.getPort()));
            logger.info("客户端发送了建立连接的请求: " + clientConfig);
            // 判断是否结束
            while (!flag) {
                try {
                    // 等待
                    int length = selector.select(2000);
                    if (length > 0) {
                        Set<SelectionKey> selectedKeys = selector.selectedKeys();
                        Iterator<SelectionKey> iterator = selectedKeys.iterator();
                        // 获取键并处理
                        while (iterator.hasNext()) {
                            SelectionKey key = iterator.next();
                            iterator.remove();
                            handleKey(key);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    logger.error("服务器的多路复用器出现问题: " + clientConfig);
                }
            }
            logger.info("客户端断开连接并且停止客户端程序: " + clientConfig);
        } catch (IOException e) {
            e.printStackTrace();
            logger.error("客户端启动异常: " + clientConfig + ", 异常信息" + e.getMessage());
        } finally {
            IoUtil.close(selector);
        }
    }

    /**
     * 处理选择键
     *
     * @param key 键
     */
    private void handleKey(SelectionKey key) {
        // 判断键是否可用
        if (!key.isValid()) {
            return;
        }

        // 判断是否可连接
        if (key.isConnectable()) {
            connect(key);
        }

        // 判断是否可读取数据
        if (key.isReadable()) {
            read(key);
        }

    }

    /**
     * 读取数据
     *
     * @param key 键
     */
    private void read(SelectionKey key) {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            // 处理服务器返回来的结果
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int length;
            while ((length = clientChannel.read(buffer)) != -1) {
                os.write(buffer.array(), 0, length);
            }
            String response = os.toString(StandardCharsets.UTF_8);
            logger.info(clientConfig + "响应内容: " + response);

            // 取消键
            key.cancel();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IoUtil.close(clientChannel);
            // 读取结束
            flag = true;
        }
    }

    /**
     * 连接
     *
     * @param key 键
     */
    private void connect(SelectionKey key) {
        try {
            SocketChannel clientChannel = (SocketChannel) key.channel();
            boolean isSuccess = clientChannel.finishConnect();
            logger.info("客户端连接服务器成功: " + clientConfig);
            if (isSuccess) {
                // 等待服务器发送的内容过来
                accept(key);
                // 给服务器发送请求
                writeDataToServer(clientChannel, data);
            } else {
                // 连接失败的处理:  1、重试 2、放弃
                logger.error("连接服务器失败: " + clientConfig);
            }
        } catch (IOException e1) {
            e1.printStackTrace();
            logger.error("连接服务器失败: " + clientConfig);
        }
    }


    /**
     * 可访问键
     *
     * @param key 键
     */
    private void accept(SelectionKey key) {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        // 注册
        NioUtil.registerChannel(key.selector(), clientChannel, Operation.READ);
    }

    /**
     * 写数据到服务器
     *
     * @param clientChannel 通道
     * @param data          数据
     */
    private void writeDataToServer(SocketChannel clientChannel, String data) {
        try {
            byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
            // 你在构造这个buffer的时候，是处于写模式下的。
            ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
            buffer.put(bytes);
            // 把buffer由写模式 切换到 读模式
            buffer.flip();
            // 把 buffer 中的 数据 发送到 服务端
            clientChannel.write(buffer);
            logger.info("发送到" + clientConfig + "请求数据成功: " + data);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("发送数据到服务器失败: " + clientConfig);
        }
    }

    /**
     * 设置待发送数据
     *
     * @param data 待发送数据
     */
    public void setData(String data) throws ValidationException {
        // 校验发送的内容是否为空
        if (StringUtil.isEmpty(data)) {
            throw new ValidationException(ResultCode.SOCKET_CLIENT_DATA_ERROR);
        }
        this.data = data;
    }
}